package Proxy;

use strict;
use IO::Socket;
use IO::Select;
use Log;
use Digest::MD5 qw(md5_hex);

my $auth_login="SOCK";
my $auth_pass=md5_hex("SOCK");

sub new
{
	my $caller=shift;
	my $class=ref($caller)||$caller;

	my $self={};
	bless($self,$class);

	my %arg=@_;
	for(keys %arg)
	{
		$self->{$_}=$arg{$_};
	}
	$self->{pidlist}=[];
	
	return $self;
}

sub destroy
{
	my $self=shift;
	$self->{active}=0;
}

sub start
{
	my $self=shift;
	my $pid;
	if($pid=fork)
	{
		push @{$self->{pidlist}},$pid;
		#is parent, should return
		#waitpid($pid,0);
	}else{
		if(defined $pid)
		{
			$self->waitConn(@_);
		}else{
			Log::_debug("Fork error $!\n");
		}
	}
}

sub initListener
{
	my $self=shift;

	# check if we need root
	if($self->{'local_port'} < 1024)
	{
		Log::_debug("Need to be root to create a socket with port < 1024.");
	}

	# setup the proxy socket
	$self->{'listener'}=IO::Socket::INET->new(
									LocalAddr => $self->{'local_host'},
									LocalPort => $self->{'local_port'},
									Listen    => Socket::SOMAXCONN,
									ReuseAddr => 1,
									Proto => 'tcp',
			) or Log::_debug("Can't open socket on ".$self->{'local_host'}.":".$self->{'local_port'}.": $!\n");

	# set autoflush
	$self->{'listener'}->autoflush(1);

	Log::_debug("Listener started on ".$self->{'local_host'}.":".$self->{'local_port'}."\n");
}

sub defaultinit
{
	my ($self)=@_;
	my ($dhost,$dport);
	if($self->{'remote_host'}) {
		$dhost=$self->{'remote_host'};
		$dport=$self->{'remote_port'};
	}else{
		$dport=$self->{'client'}->sockport();
		$dhost=$self->{'client'}->sockhost();
	}

	Log::_debug("Connecting to ",$dhost,":",$dport."\n");

	$self->{'remote'}=IO::Socket::INET->new(
										PeerAddr => $dhost,
										PeerPort => $dport,
										Proto    => 'tcp',
	) or Log::_debug("Can't connect to $dhost:$dport: $!\n");
}

sub defaultterm
{
	my $self=shift;
	$self->{'remote'}->close();
}

sub defaultin
{
	my $self=shift;
	for my $sock (keys %{$self->{'list'}})
	{
		my $sp=$self->{'list'}->{$sock};
		my $buf;
		my $sel=IO::Select->new($sp->{'remote'});
		while($sel->can_read(0.05))
		{
			last unless $sp->{'remote'}->sysread($buf,1024,length($buf));
		}

		my $err=0;
		if(!$buf)
		{
			Log::_debug("Session closed from remote side ",$sock->sockhost(),":",$sock->sockport());
			$err=1;
		}else{
			select($sock);
			$|=1;
			print $buf;
			select(STDOUT);
		}

		if($err)
		{
			$self->{'termfunc'}->($sp);
			$self->{'select'}->remove($sock);
			delete $self->{'list'}->{$sock};
			$sock->close();
		}
	}
}

sub defaultout
{
	my ($self,$d)=@_;
	select($self->{'remote'});
	$|=1;
	print $d;
	select(STDOUT);
}

sub waitConn
{
	my ($self,$local_host,$local_port,$infunc,$outfunc,$initfunc,$termfunc)=@_;
	$self->{'local_host'}=$local_host;
	$self->{'local_port'}=$local_port;
	$self->{'infunc'}=$infunc||\&defaultin;
	$self->{'outfunc'}=$outfunc||\&defaultout;
	$self->{'initfunc'}=$initfunc||\&defaultinit;
	$self->{'termfunc'}=$termfunc||\&defaultterm;
	my ($client,$pid);

	$self->initListener();
	
	$self->{'list'}={};
	
	$self->{'select'}=IO::Select->new;
	$self->{'select'}->add($self->{'listener'});

	while(1)
	{
		#check client and listener
		my @readable = $self->{'select'}->can_read(0.05);
		for my $sock (@readable)
		{
			my $pass=0;
			if($sock == $self->{'listener'})
			{
				my $client=$sock->accept();
				$client->autoflush(1);
				my $cp={'client'=>$client,'proxy'=>$self};
				Log::_debug("Connect from ",$cp->{'client'}->peerhost(),':',$cp->{'client'}->peerport()."\n");

				my $buf;
				my $sel=IO::Select->new($client);
				while($sel->can_read(0.05))
				{
					last unless $client->sysread($buf,1024,length($buf));
				}

				my @data=unpack("C*",$buf);
				my $err=0;

				#SOCK4
				if($data[0] == 4)
				{
					#  1 #  2 #  3 #  4 #  5 #  6 #  7 #  8 #  9 # 10 # 11 #    ... #
					#################################################################
					#VERS#TYPE#   PORT  #         IP        #         USER      ...0#
					#################################################################
					#VERS:
					#	4
					#TYPE:
					#	1 - CONNECT
					#	2 - BIND
					Log::_debug("Proxy SOCKS v4\n");
					my $destip=pack("N",0);
					my $destport=0;
					my @head=();
					push @head,shift @data,shift @data,shift @data,shift @data;
					my @ip=();
					push @ip,shift @data,shift @data,shift @data,shift @data;
					$destip=inet_ntoa(pack("C4",@ip));
					$destport=$head[2]<<8 | $head[3];
					my $user='';
					while(@data > 0) {
						my $b=shift @data;
						last unless($b);
						$user.=chr($b);
					}

					if($head[1] == 1)
					{
						$cp->{'remote_host'}=$destip;
						$cp->{'remote_port'}=$destport;
						$self->{'initfunc'}->($cp);
					}elsif($head[1] == 2){
						#not implemented
						$err=1;
					}
				#SOCK5
				}elsif($data[0] == 5){
					Log::_debug("Proxy SOCKS v5\n");
					my $t=$data[1];
					shift @data;
					shift @data;
					if(@data < $t)
					{
						$err=1;
					}else{
						my @auth=();
						for(1..$t)
						{
							push @auth,shift @data;
						}
						my $success=0;
						for my $i (0..($t-1))
						{
							my $ord=$auth[$i];
							if($ord == 0 && !$auth_login)
							{
								$client->syswrite("\x05\x00", 2);
								$success++;
								last;
							}elsif($ord == 1 && $auth_login){
								#GSSAPI auth support
								#$success++;
								#last;
							}elsif($ord == 2 && $auth_login){
								if(do_auth($sock))
								{
									$success++;
									last;
								}else{
									$err;
								}
							}
						}
						if($success)
						{
							my $buff;
							$t=$client->sysread($buff,3);
							if(substr($buff,0,1) eq "\x05")
							{
								if(ord(substr($buff,2,1)) == 0)
								{
									my ($host,$raw_host)=socks_get_host($client);
									if(!$host)
									{
										$err=1;
									}else{
										my ($port,$raw_port)=socks_get_port($client);
										if(!$port)
										{
											$err=1;
										}else{
											my $ord=ord(substr($buff,1,1));
											$buff="\x05\x00\x00".$raw_host.$raw_port;
											$client->syswrite($buff,length($buff));
											if($ord == 1)
											{
												$cp->{'remote_host'}=$host;
												$cp->{'remote_port'}=$port;
												$self->{'initfunc'}->($cp);
											}elsif($ord == 2){
												#not implemented
												$err=1;
											}elsif($ord == 3){
												#not implemented
												$err=1;
											}
										}
									}
								}
							}
						}else{
							$client->syswrite("\x05\xFF",2);
						}
					}
				#HTTP
				}else{
					$cp->{'remote_host'}="0";
					$cp->{'remote_port'}=0;
					$self->{'initfunc'}->($cp);
					$self->{'outfunc'}->($cp,$buf);
					$pass=1;
				}

				if(!$err)
				{
					$self->{'select'}->add($client);
					$self->{'list'}->{$client}=$cp;
				}else{
					$pass=1;
				}

				$sock=$client;
			}
			if(!$pass)
			{
				if(defined $self->{'list'}->{$sock})
				{
					my $cp=$self->{'list'}->{$sock};
					my $buf;
					my $sel=IO::Select->new($sock);
					while($sel->can_read(0.05))
					{
						last unless $sock->sysread($buf,1024,length($buf));
					}
					my $err=0;
					if(!$buf)
					{
						Log::_debug("Session closed from local side ",$sock->peerhost(),":",$sock->peerport());
						$err=1;
					}else{
						$self->{'outfunc'}->($cp,$buf);
					}
					if($err)
					{
						$self->{'termfunc'}->($cp);
						$self->{'select'}->remove($sock);
						delete $self->{'list'}->{$sock};
						$sock->close();
					}
				}else{
					$self->{'select'}->remove($sock);
				}
			}
		}
		
		#check remote transport
		$self->{'infunc'}->($self);
	}

	Log::_debug("Listener terminated\n");

	return;
}

sub do_auth
{
	my($buff,$login,$pass);
	my $client=$_[0];
	$client->syswrite("\x05\x02",2);
	$client->sysread($client,$buff,1);
	if(ord($buff) == 1)
	{
		$client->sysread($buff,1);
		$client->sysread($login,ord($buff));
		$client->sysread($buff,1);
		$client->sysread($pass,ord($buff));
		if($login eq $auth_login && md5_hex($pass) eq $auth_pass)
		{
			$client->syswrite("\x05\x00",2);
			return 1;
		}else{
			$client->syswrite("\x05\x01",2);
		}
	}
	return 0;
}

sub socks_get_host
{
	my $client=$_[0];
	my ($t,$ord,$raw_host);
	my $host="";
	my @host;

	$client->sysread($t,1);
	$ord=ord($t);
	if($ord == 1)
	{
		$client->sysread($raw_host,4);
		@host=($raw_host=~/(.)/g);
		$host=ord($host[0]).".".ord($host[1]).".".ord($host[2]).".".ord($host[3]);
	}elsif($ord == 3){
		$client->sysread($raw_host,1);
		$client->sysread($host,ord($raw_host));
		$raw_host.=$host;
	}elsif($ord == 4){
		#ipv6
	}
	return ($host,$t.$raw_host);
}

sub socks_get_port
{
	my $client=$_[0];
	my ($raw_port,$port);
	$client->sysread($raw_port,2);
	$port=ord(substr($raw_port,0,1))<<8 | ord(substr($raw_port,1,1));
	return ($port,$raw_port);
}

1;